/*
 * MicroEmulator 
 * Copyright (C) 2001 Bartek Teodorczyk <barteo@barteo.net>
 * 
 *  It is licensed under the following two licenses as alternatives:
 *    1. GNU Lesser General Public License (the "LGPL") version 2.1 or any newer version
 *    2. Apache License (the "AL") Version 2.0
 *
 *  You may not use this file except in compliance with at least one of
 *  the above two licenses.
 *
 *  You may obtain a copy of the LGPL at
 *      http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt
 *
 *  You may obtain a copy of the AL at
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the LGPL or the AL for the specific language governing permissions and
 *  limitations.
 */

package javax.microedition.lcdui;

import java.util.ArrayList;
import org.microemu.util.StringUtils;
import org.microemu.device.DeviceFactory;

class StringComponent {

    private static final char NEWLINE_SEPARATOR = '\n';
    
	private String text;

	/* 
	 * BlueWhaleSystems fix: Ramesh Nair - 16 May 2008
	 */
	private volatile int breaks[] = new int[4];
	private volatile TextPortion lines[] = null;
	private boolean invertPaint = false;

	/* 
	 * BlueWhaleSystems fix: Ramesh Nair - 16 May 2008
	 */
	private volatile int numOfBreaks;
    private volatile int numOfLines;

	private int width;

	private int widthDecreaser;

	
	private volatile int highlightedPortionStartCaretPosition = -1;
	private volatile int highlightedPortionEndCaretPosition = -1;
	
	
	public StringComponent() {
		this(null);
	}

	public StringComponent(String text) {
		synchronized (this) {
			this.width = -1;
			this.widthDecreaser = 0;
			setText(text);
			stopHighlightingText();
		}
	}

	public int getCharHeight() {
		return Font.getDefaultFont().getHeight();
	}

	public int getCharPositionX(int num) {
		synchronized (this) {
			if (numOfBreaks == -1) {
				updateBreaksAndLines();
			}
	
			int i, prevIndex = 0;
			Font f = Font.getDefaultFont();
	
			for (i = 0; i < numOfBreaks; i++)
			{
                /**
                 * Caret should be at end of line for newline chars 
                 * BlueWhaleSystems fix: Ramesh Nair - 15 May 2008
                 */
				if ( num < breaks[i] || (num == breaks[i] && NEWLINE_SEPARATOR == text.charAt( num )) )
				{
					break;
				}
				prevIndex = breaks[i];
			}
			
			return f.substringWidth(text, prevIndex, num - prevIndex);
		}
	}

	public int getCharPositionY(int num) {
		int y = 0;
		synchronized (this) {
			if (numOfBreaks == -1) {
				updateBreaksAndLines();
			}
	
			Font f = Font.getDefaultFont();
	
			for (int i = 0; i < numOfBreaks; i++)
			{
                /**
                 * Caret should be at end of line for newline chars 
                 * BlueWhaleSystems fix: Ramesh Nair - 15 May 2008
                 */
                if ( num < breaks[i] || (num == breaks[i] && NEWLINE_SEPARATOR == text.charAt( num )) )
				{
					break;
				}
				y += f.getHeight();
			}
		}

		return y;
	}

	
    /**
     * Get the expected position of the caret if moved to the end of the 
     * current row of text.
     * @param caretPosition the current character position of the caret.
     * @return the new caret position.
     * 
     * BlueWhaleSystems fix: Ramesh Nair - 15 May 2008
     */
	int getCaretPositionEndOfCurrentRow(int caretPosition)
    {
        synchronized (this)
        {
            if (numOfBreaks == -1) {
                updateBreaksAndLines();
            }
    
            int i;
    
            for (i = 0; i < numOfLines; ++i)
            {
                if (lines[i].endCaret() >= caretPosition)
                {
                    break;
                }
            }
            
            // if there are no line breaks or we're on the bottom row
            if (0 == numOfLines || numOfLines <= i)
            {
                return text.length();
            }
            else
            {
                return lines[i].endCaret();
            }
            
        }       
    }
    
    
    /**
     * Get the expected position of the caret if moved to the beginning of the 
     * current row of text.
     * @param caretPosition the current character position of the caret.
     * @return the new caret position.
     * 
     * BlueWhaleSystems fix: Ramesh Nair - 15 May 2008
     */
	int getCaretPositionBeginningOfCurrentRow(int caretPosition)
    {
        synchronized (this)
        {
            if (numOfBreaks == -1) {
                updateBreaksAndLines();
            }
    
            int i;
    
            for (i = 0; i < numOfLines; ++i)
            {
                if (lines[i].endCaret() >= caretPosition)
                {
                    break;
                }
            }
            
            // if there are no line breaks or we're on the bottom row
            if (0 == numOfLines)
            {
                return 0;
            }
            else if (numOfLines <= i)
            {
                return lines[numOfLines - 1].startCaret();
            }
            else
            {
                return lines[i].startCaret();
            }
            
        }       
    }    
    
    
    
    
	/**
	 * Get the expected position of the caret if moved up one row of text.
	 * @param caretPosition the current character position of the caret.
	 * @return less than 0 if caret is already on the top row of text.
	 * 
	 * BlueWhaleSystems fix: Ramesh Nair - 9,16 May 2008
	 */
	int getCaretPositionOneRowUp(int caretPosition)
	{
        synchronized (this)
        {
            if (-1 == numOfBreaks) {
                updateBreaksAndLines();
            }
    
            int i;
            
            for (i = 0; i < numOfLines; ++i)
            {
                if (lines[i].endCaret() >= caretPosition)
                {
                    break;
                }
            }
            
            // if we're already on the top row
            if (0 == i)
            {
                return -1;
            }
            else if (numOfLines <= i)
            {
                return lines[numOfLines-1].startCaret();
            }
            else
            {
                return lines[i-1].startCaret() + Math.min( lines[i-1].length(), caretPosition - lines[i].startCaret() );
            }
        }
	}
	
	
    /**
     * Get the expected position of the caret if moved down one row of text.
     * @param caretPosition the current character position of the caret.
     * @return less than 0 if caret is already on the bottom row of text.
     * 
     * BlueWhaleSystems fix: Ramesh Nair - 9,16 May 2008
     */
    int getCaretPositionOneRowDown(int caretPosition)
    {
        synchronized (this)
        {
            if (numOfBreaks == -1) {
                updateBreaksAndLines();
            }
    
            int i;
            
            for (i = numOfLines-1; i >= 0; --i)
            {
                if (lines[i].startCaret() <= caretPosition)
                {
                    break;
                }
            }
            
            // if we're already on the bottom row
            if (numOfLines-1 == i)
            {
                return -1;
            }
            else if (0 > i)
            {
                return lines[0].startCaret();
            }
            else
            {
                return lines[i+1].startCaret() + Math.min( lines[i+1].length(), caretPosition - lines[i].startCaret() );
            }
            
        }       
    }
    
    
    
    /**
     * Get the expected position of the caret if moved right one column.
     * @param caretPosition the current character position of the caret.
     * 
     * BlueWhaleSystems fix: Ramesh Nair - 15 May 2008
     */
    int getCaretPositionOneColumnRight(int caretPosition)
    {
        synchronized (this)
        {
            if (numOfBreaks == -1) {
                updateBreaksAndLines();
            }

            if (text.length() <= caretPosition)
            {
                return text.length();
            }
            
            ++caretPosition;
        }
        
        return caretPosition;
    }    
    
    /**
     * Get the expected position of the caret if moved left one column.
     * @param caretPosition the current character position of the caret.
     * 
     * BlueWhaleSystems fix: Ramesh Nair - 15 May 2008
     */
    int getCaretPositionOneColumnLeft(int caretPosition)
    {
        synchronized (this)
        {
            if (numOfBreaks == -1) {
                updateBreaksAndLines();
            }
            
            if (0 >= caretPosition)
            {
                return 0;
            }

            --caretPosition;
        }
        
        return caretPosition;
    } 

    
	public int getHeight() {
		int height;
		synchronized (this) {
			if (numOfBreaks == -1) {
				updateBreaksAndLines();
			}

			Font f = Font.getDefaultFont();

			if (text == null) {
				return 0;
			}

			if (numOfBreaks == 0) {
				return f.getHeight();
			}

			height = numOfBreaks * f.getHeight();

			if (breaks[numOfBreaks - 1] == text.length() - 1
					&& text.charAt(text.length() - 1) == '\n') {
				// TODO: Michael Busheikin, 25/3/09
				// TODO: I don't understand the point of this if/else block
                height += f.getHeight();
			} else {
				height += f.getHeight();
			}
		}

		return height;
	}

	public String getText() {
		return text;
	}

	
    /**
     * BlueWhaleSystems fix: Ramesh Nair - 14 May 2008
     */	
	public String getHighlightedText()
    {
        TextPortion marker = getNormalisedHighlightMarker();
        if (0 < marker.length())
        {
            return text.substring( marker.startCaret(), marker.endCaret() );
        }
        return null;
    }

    
    /**
     * BlueWhaleSystems fix: Ramesh Nair - 14 May 2008
     */     
	public void deleteHighlightedText()
    {
        synchronized(this)
        {
            TextPortion marker = getNormalisedHighlightMarker();
            if (0 < marker.length())
            {
                text = StringUtils.substring( text, 0, marker.startCaret() ) + 
                            StringUtils.substring( text, marker.endCaret() );
                stopHighlightingText();
                numOfBreaks = -1;
                numOfLines = -1;
            }        
        }
    }
    
    
    public void invertPaint(boolean state) {
		synchronized (this) {
			invertPaint = state;
		}
	}

	public int paint(Graphics g) {
		if (text == null) {
			return 0;
		}

		int y;
		synchronized (this)
		{
            /**
             * BlueWhaleSystems fix: Ramesh Nair - 13-15 May 2008
             * 
             * Highlighted text should be shown inverted.
             */
		    final int COLOR_BLACK = 0;
            final int COLOR_WHITE = 255;
		    
            int normalBgColor = (invertPaint ? COLOR_BLACK : COLOR_WHITE);
            int normalFgColor = (invertPaint ? COLOR_WHITE : COLOR_BLACK);
            
            int highlightBgColor = (COLOR_BLACK == normalBgColor ? COLOR_WHITE : COLOR_BLACK);
            int highlightFgColor = (COLOR_BLACK == normalFgColor ? COLOR_WHITE : COLOR_BLACK);
            
            TextPortion marker = getNormalisedHighlightMarker();
            
			if (-1 == numOfBreaks)
			{
				updateBreaksAndLines();
			}
	
			Font f = Font.getDefaultFont();
			int i = -1;
			y = 0;
			int prevBreak = 0;
			int nextBreak = 0;
	
			while (text.length() > prevBreak)
			{
			    ++i;
			    if (numOfBreaks <= i)
			    {
			        nextBreak = text.length();
			    }
			    else
			    {
			        nextBreak = breaks[i];
			    }
			    
			    // if there is something to highlight and highlight starts/started before the next line break and has not yet ended
			    if (0 < marker.length() && marker.startCaret() <= nextBreak && marker.endCaret() >= prevBreak)
			    {
	                int x = 0;

	                // if highlighted section starts on current row
			        if (marker.startCaret() >= prevBreak)
			        {
		                x += paintTextPortion( g, f, prevBreak, marker.startCaret() - prevBreak, x, y, normalBgColor, normalFgColor );
		                
	                    // if highlighted section finishes before next break
	                    if (marker.endCaret() < nextBreak)
	                    {
	                        x += paintTextPortion( g, f, marker.startCaret(), marker.length(), x, y, highlightBgColor, highlightFgColor );
	                        x += paintTextPortion( g, f, marker.endCaret(), nextBreak - marker.endCaret(), x, y, normalBgColor, normalFgColor );
	                    }
	                    // if highlighted section doesn't finish before next break
	                    else
	                    {
                            x += paintTextPortion( g, f, marker.startCaret(), nextBreak - marker.startCaret(), x, y, highlightBgColor, highlightFgColor );
	                    }
			        }
                    // if the continuing highlighted section covers the entire current row
			        else if (marker.endCaret() >= nextBreak)
			        {
                        x += paintTextPortion( g, f, prevBreak, nextBreak - prevBreak, x, y, highlightBgColor, highlightFgColor );
			        }
			        // if the continuing highlighted section finishes before next break
			        else if (marker.endCaret() < nextBreak) 
			        {
                        x += paintTextPortion( g, f, prevBreak, marker.endCaret() - prevBreak, x, y, highlightBgColor, highlightFgColor );
                        x += paintTextPortion( g, f, marker.endCaret(), nextBreak - marker.endCaret(), x, y, normalBgColor, normalFgColor );
			        }
			    }
			    // either there is nothing to highlight or highlighted section hasn't yet started or highlighted section has already been done
			    else
			    {
                    paintTextPortion( g, f, prevBreak, nextBreak - prevBreak, 0, y, normalBgColor, normalFgColor );			        
			    }

			    prevBreak = nextBreak;
			    y += f.getHeight(); 
			    
			} // while more rows left to be processed
			
		}

		return y;
	}

	
	/**
	 * Draw a portion of text.
	 * 
	 * @return the width of the drawn segment.
	 * 
     * BlueWhaleSystems fix: Ramesh Nair - 14 May 2008
	 */
	private int paintTextPortion(Graphics g, Font f, int index, int length, int x, int y, int bgColor, int fgColor)
	{
	    if (0 > index || text.length() <= index || 0 >= length) 
	        return 0;
	        
	    int txtWidth = f.substringWidth( text, index, length );
	    
        g.setGrayScale(bgColor);
        g.fillRect(x, y, txtWidth, f.getHeight());

        g.setGrayScale(fgColor);
        g.drawSubstring(text, index, length, x, y, 0);
        
        return txtWidth;
	}
	
	
	
	
    /**
     * Get caret position for given (x,y) coordinate.
     * 
     * BlueWhaleSystems fix: Ramesh Nair - 15 May 2008
     */	
	int getCaretPositionForXY(int x, int y)
	{
        synchronized (this) {
            if (-1 == numOfBreaks) {
                updateBreaksAndLines();
            }

            Font f = Font.getDefaultFont();
//System.out.println(f.getHeight() + ", x=" + x + ", y=" + y);      

            if (0 == numOfLines)
            {
                return 0;
            }
            
            int cumheight = 0;
            
            for (int i=0; i<numOfLines; ++i)
            {
                cumheight += f.getHeight();
                if (cumheight >= y)
                {
                    int widthSoFar = 0;
                    for (int c=1; c<=lines[i].length(); ++c)
                    {
                        widthSoFar = f.substringWidth( text, lines[i].startCaret(), c );
                        if (widthSoFar > x)
                        {
                            return lines[i].startCaret() + c - 1;
                        }
                    }
                    
                    // if reached this point then set caret to end of line
                    return lines[i].endCaret();
                }
            }

            return text.length();
        }
	}
	
	
	
	public void setText(String text) {
		synchronized (this) {
			this.text = text;
			this.numOfBreaks = -1;
			this.numOfLines = -1;
		}
	}

	public void setWidthDecreaser(int widthDecreaser) {
		synchronized (this) {
			this.widthDecreaser = widthDecreaser;
			numOfBreaks = -1;
            numOfLines = -1;
		}
	}

	/* 
	 * BlueWhaleSystems fix: Ramesh Nair - 13 May 2008
	 *
	 * Work towards text highlighting.
	 */
	
	void stopHighlightingText()
    {
	    synchronized( this )
        {
	        highlightedPortionStartCaretPosition = -1;
	        highlightedPortionEndCaretPosition = -1;
        }
    }
    
    /**
     * Get normalised highlight start and end points.
     * 
     * This will ensure that start point comes before end point, etc.
     * 
     * @return marker with length0 if we're not currently highlighting any text 
     * or if highlight points are out of range.
     * 
     * BlueWhaleSystems fix: Ramesh Nair - 14 May 2008
     */
    private TextPortion getNormalisedHighlightMarker()
    {
        if (!weAreCurrentlyHighlightingText())
        {
            return new TextPortion(-1,-1);
        }
        
        int highlightStartPoint = -1;
        int highlightEndPoint = -1;
        synchronized(this)
        {
            highlightStartPoint = highlightedPortionStartCaretPosition;
            highlightEndPoint = highlightedPortionEndCaretPosition;
        }
        if (highlightEndPoint < highlightStartPoint)
        {
            // swap numbers using XOR
            highlightStartPoint ^= highlightEndPoint;
            highlightEndPoint ^= highlightStartPoint;
            highlightStartPoint ^= highlightEndPoint;
        }
     
        if (highlightStartPoint >= text.length())
        {
            return new TextPortion(-1,-1);
        }
        
        return new TextPortion(highlightStartPoint, highlightEndPoint);
    }
    	
    
    /**
     * BlueWhaleSystems fix: Ramesh Nair - 13-14 May 2008
     */
    boolean weAreCurrentlyHighlightingText()
    {
        return (0 <= highlightedPortionStartCaretPosition && 0 <= highlightedPortionEndCaretPosition);
    }    

	/* 
	 * BlueWhaleSystems fix: Ramesh Nair - 13 May 2008
	 *
	 * Work towards text highlighting.
	 */
	void markHighlightedTextEndPoint(int caretPosition)
	{
        synchronized( this )
        {
            if (weAreCurrentlyHighlightingText())
            {
                highlightedPortionEndCaretPosition = caretPosition;
            }
        }	    
	}
	
	/* 
	 * BlueWhaleSystems fix: Ramesh Nair - 13 May 2008
	 *
	 * Work towards text highlighting.
	 */
	void markHighlightedTextStartPoint(int caretPosition)
    {
	    synchronized( this )
        {
            highlightedPortionStartCaretPosition = caretPosition;
            highlightedPortionEndCaretPosition = caretPosition;
        }
	    
    }
	
    /**
     * Get caret position of start point of highlighted text.
     * 
     * @return -1 if no text is currently highlighted.
     * 
     * BlueWhaleSystems fix: Ramesh Nair - 14 May 2008
     */
	int getHighlightedTextStartCaret()
    {
        TextPortion marker = getNormalisedHighlightMarker();
        if (0 < marker.length())
        {
            return marker.startCaret();
        }
        return -1;
    }
	
	/**
	 * Get whether the text has line breaks in it.
	 * @return true if it does; false otherwise.
	 * 
     * BlueWhaleSystems fix: Ramesh Nair - 14 May 2008
	 */
	boolean hasBreaks()
	{
	    synchronized(this)
	    {
            if (numOfBreaks == -1)
            {
                updateBreaksAndLines();
            }
            return 0 < numOfBreaks;
	    }
	}
	
	private void insertBreak(int pos) {
		int i;

		for (i = 0; i < numOfBreaks; i++) {
			if (pos < breaks[i]) {
				break;
			}
		}
		if (numOfBreaks + 1 == breaks.length) {
			int newbreaks[] = new int[breaks.length + 4];
			System.arraycopy(breaks, 0, newbreaks, 0, numOfBreaks);
			breaks = newbreaks;
		}
		System.arraycopy(breaks, i, breaks, i + 1, numOfBreaks - i);
		breaks[i] = pos;
		numOfBreaks++;
	}

	private void updateBreaksAndLines() {
		if (text == null) {
			return;
		}

		// TODO use Displayable width
		width = DeviceFactory.getDevice().getDeviceDisplay().getWidth()
				- widthDecreaser;

		int prevIndex = 0;
		int canBreak = 0;
		numOfBreaks = 0;
		Font f = Font.getDefaultFont();

		for (int i = 0; i < text.length(); i++) {
			if (' ' == text.charAt(i)) {
				canBreak = i + 1;
			}
			if (NEWLINE_SEPARATOR == text.charAt(i)) {
				insertBreak(i);
				canBreak = 0;
				prevIndex = i + 1;
				continue;
			}
			if (f.substringWidth(text, prevIndex, i - prevIndex + 1) > width) {
				if (canBreak != 0) {
					insertBreak(canBreak);
					i = canBreak;
					prevIndex = i;
				} else {
					insertBreak(i);
					prevIndex = i + 1;
				}
				canBreak = 0;
			}
		}
		
		updateLines();
	}
	
	
    
    /**
     * Re-populate the {@link #lines} array.
     * 
     * BlueWhaleSystems fix: Ramesh Nair - 15-16 May 2008
     */ 
    private void updateLines()
    {
        synchronized (this)
        {
            if (-1 == numOfBreaks)
            {
                return;
            }
            
            ArrayList list = new ArrayList();
            
            int i = -1;
            int lineStart = 0;
            
            while (++i < numOfBreaks)
            {
                if (NEWLINE_SEPARATOR == text.charAt(breaks[i]))
                {
                    /*
                     * Case:
                     *      ...\n
                     *      \n    <-
                     *      ...
                     *  
                     * Case:
                     *      \n    <-
                     *      ...
                     *      ...
                     */
                    if ( (0 <= breaks[i] - 1 && NEWLINE_SEPARATOR == text.charAt( breaks[i] - 1 )) || 0 == breaks[i] )
                    {
                        list.add( new TextPortion(breaks[i], breaks[i]) );
                        lineStart = breaks[i] + 1;
                        continue;
                    }
                    /*
                     * Case:
                     *      ...
                     *      ...\n  <-
                     *      ...
                     */
                    else
                    {
                        list.add( new TextPortion(lineStart, breaks[i]) );
                        lineStart = breaks[i] + 1;
                        continue;
                    }
                }
                else
                {
                    /*
                     * Case:
                     *      (.)..   <-
                     */
                    list.add( new TextPortion(lineStart, breaks[i]-1) );
                    lineStart = breaks[i];
                    continue;
                }
            }
            
            
            if (text.length() > lineStart)
            {
                list.add( new TextPortion(lineStart, text.length()) );
            }
            
            TextPortion[] tmp = new TextPortion[0];
            lines = (TextPortion[])list.toArray( tmp );
            numOfLines = lines.length;
        }
    }	
	
	
	
	


	/**
     * BlueWhaleSystems fix: Ramesh Nair - 14 May 2008
	 */
	private class TextPortion
	{
	    private int startCaret = -1;
	    private int endCaret = -1;
	    
	    public TextPortion(int startCaret, int endCaret)
	    {
	        this.startCaret = startCaret;
	        this.endCaret = endCaret;
	        if (this.endCaret < this.startCaret)
	        {
	            this.endCaret = this.startCaret;
	        }
	    }

        public int startCaret()
        {
            return startCaret;
        }

        public int endCaret()
        {
            return endCaret;
        }
	    
        
        public int length()
        {
            if (0 <= startCaret)
            {
                return endCaret - startCaret;
            }
            else
                return 0;
        }
	    
	}

}
